/**
 Created by:
 muhtearjan mehmood مۇختەرجان مەخمۇت (Email: yeganaaa@163.com)
 At 8/5/20 12:31 AM
 */
import {MetaData, MetaOfModule, MetaType} from "./MetaData";
import {v4 as uuid} from "uuid"

let defaultTextEncoder: TextEncoder
let defaultTextDecoder: TextDecoder
/**
 * set default text encoder used by RpcBridge
 * @remarks
 * this is usually use when some JavaScript runtime environment that has no TextEncoder class implementation, then we should implement it and supply through this function.
 * @param encoder an instance of {@link TextEncoder}
 */
export function setDefaultTextEncoder(encoder: TextEncoder) {
    defaultTextEncoder = encoder
}
/**
 * set default @TextDecoder used by RpcBridge
 * @remarks 
 * this is usually use when some JavaScript runtime environment that has no TextEncoder class implementation, then we should implement it and supply through this function.
 * @param decoder an instance of {@link TextDecoder}
 */
export function setDefaultTextDecoder(decoder: TextDecoder) {
    defaultTextDecoder = decoder
}
/**
 * Initialize default {@link TextEncoder}/{@link TextDecoder} when they are not supplied yet.
 */
function _initializeDefaultEncoderAndDecodersIfHasNotInitialized() {
    if (defaultTextEncoder == null) setDefaultTextEncoder(new TextEncoder())
    if (defaultTextDecoder == null) setDefaultTextDecoder(new TextDecoder())
}
/**
 * Binary data fragments
 * @remarks
 * {@link Fragment} is a wrapper class that used to chain one or more binary data array to one binary data array.
 * we can using this class to chain one or more binary data and put them into single {@link ragment} class instance using encode method or get all chained binary data from {@link Fragment} class instance using fromBytes method.
 */
export class Fragment {
    constructor(public data: Uint8Array) {
    }
    /**
     * convert fragment to binary data
     * @returns {@link Uint8Array} instance of binary data
     */
    public encode() {
        const encoded = new Uint8Array(this.data.byteLength + 4)
        let pointer = 0
        const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength)
        view.setUint32(pointer, this.data.byteLength, true)
        pointer += 4
        encoded.set(this.data, pointer)
        return encoded
    }
    /**
     * Extract one or more Fragment instance from binary data
     * @param bytes binary data
     * @param offset Byte offset in the supplied binary data
     */
    public static fromBytes(bytes: Uint8Array, offset: number = null!): Iterable<Fragment> {
        if (offset == null) offset = bytes.byteOffset
        return new class implements Iterable<Fragment> {
            [Symbol.iterator](): IterableIterator<Fragment> {
                return {
                    next(...args): IteratorResult<Fragment> {
                        let hasItem = true
                        hasItem = hasItem && offset < bytes.buffer.byteLength
                        if (!hasItem) return {done: true, value: null}
                        const view = new DataView(bytes.buffer, offset)
                        const length = view.getUint32(0, true)
                        const fragment = new Fragment(new Uint8Array(bytes.buffer, offset + 4, length))
                        offset += 4 + length
                        return {
                            done: false,
                            value: fragment
                        }
                    },
                    [Symbol.iterator](): IterableIterator<Fragment> {
                        return this
                    }
                }
            }
        }
    }
    /**
     * Put all fragments into single binary data array
     * @param fragments one or more instance of fragment
     * @returns binary data
     */
    public static encodeAll(...fragments: Fragment[]): Uint8Array {
        const sizes = fragments.map(value => value.data.length + 4).reduce((previousValue, currentValue) => previousValue + currentValue)
        const bytes = new Uint8Array(sizes)
        let pointer = 0
        fragments.forEach(fragment => {
            const bytesOfFragment = fragment.encode()
            bytes.set(bytesOfFragment, pointer)
            pointer += bytesOfFragment.length
        })
        return bytes
    }
}
/**
 * Completer class
 * @remarks
 * maybe you want run some operation should wait until another operation's complete, these case this class is helpful for you.
 * @example 
 * export async function delay(milliSecond: number) {
 *  const completer = Completer.create()
 *  setTimeout(completer.complete, milliSecond)
 *  return completer.promise
 * }
 * @typeParam T Value type
 */
export class Completer<T = void> {
    private action: (value?: T, error?: Error) => void
    /**
     * Promise
     */
    promise: Promise<T> = null!
    constructor() {
        this.action = error => {
            throw new Error("Please build this completer before using it")
        }
    }
    /**
     * Complete this completer with value
     * @param value value
     * @remarks
     * corresponds Promise.resolve(value).
     */
    public complete(value: T){
        this.action(value)
    }
    /**
     * Reject this completer with given error message
     * @param error error
     */
    public error(error: Error){
        this.action(null!, error)
    }
    /**
     * Initialize this completer before using
     */
    public build() {
        if (this.promise != null) throw new Error("This completer is already completed!")
        this.promise = new Promise<T>((resolve, reject) => {
            this.action = (value, error) => error == null ? resolve(value!) : reject(error)
        })
        return this.promise
    }
    /**
     * Create an instance of initialized Completer class
     */
    public static create<T = void>() {
        const value = new Completer<T>()
        value.build()
        return value
    }
}
/**
 * Wait some time
 * @remarks a Helper class for if you want to sleep some time in an async function.
 * @param milliSecond milliseconds of time
 */
export async function delay(milliSecond: number) {
    const completer = Completer.create()
    setTimeout(() => completer.complete(), milliSecond)
    return completer.promise
}
/**
 * Rpc message type
 */
enum MessageType {
    RpcRequest = 0x01,
    RpcResponse = 0x02,
    MetaRequest = 0x03,
    MetaResponse = 0x04,
}
/**
 * Base class of RpcMessage s
 */
export abstract class RpcMessage {
    public header: Map<string, any> = new Map<string, any>()
    public body: any
    constructor(public id: string, public type: MessageType) {}
    public encode(){
        return _encodeMessage(this)
    }
    public static decode(bytes: Uint8Array): RpcMessage {
        return _decodeMessage(bytes)
    }
}
/**
 * Rpc Request object
 */
export class RpcRequest extends RpcMessage {
    constructor(public id: string, public action: string) {
        super(id, MessageType.RpcRequest);
    }
}
/**
 * Rpc Response object
 */
export class RpcResponse extends RpcMessage {
    constructor(public id: string, public requestId: string, public hasError: boolean, public error?: any) {
        super(id, MessageType.RpcResponse);
    }
}
/**
 * Meta data request object
 */
export class MetaRequest extends RpcMessage{
    constructor(public meta: MetaData) {
        super("", MessageType.MetaRequest);
        this.body = meta
    }
}
/**
 * Meta data response object
 */
export class MetaResponse extends RpcMessage {
    constructor() {
        super("", MessageType.MetaResponse);
    }
}
/**
 * check given object is the one or not of [Buffer], [Uint8Array], [ArrayBuffer], [SharedArrayBuffer]
 * @param object
 */
export function isBufferObject(object: any): boolean {
    const type = object?.constructor?.name ?? ""
    return type == "Buffer" || type == "Uint8Array" || type == "ArrayBuffer" || type == "SharedArrayBuffer"
}
/**
 * convert all different buffer objects to standard Uint8Array
 * @remarks
 * useful when you want to convert Node.Js Buffer object instance or ArrayBuffer, SharedArrayBuffer objects to Uint8Array instance
 * @param buffer instance of buffer objects
 */
export function getBufferObject(buffer: Buffer | Uint8Array | ArrayBuffer | SharedArrayBuffer): Uint8Array {
    const type = buffer.constructor.name
    if (type == "Buffer") return new Uint8Array(buffer)
    if (type == "ArrayBuffer") return new Uint8Array(buffer)
    if (type == "SharedArrayBuffer") return new Uint8Array(buffer)
    return buffer as Uint8Array
}
/**
 * Intermediate and common message type to all different RpcMessages
 */
export type IntermediateMessageType = {
    id: string,
    type: MessageType,
    header: any,
    requestId?: string,
    action?: string,
    hasError?: boolean,
    error?: any
}
/**
 * encode the given RpcMessage instance to standard Uint8Array instance
 * @param message instance of RpcMessage class
 * @returns Uint8Array binary data
 */
function _encodeMessage(message: RpcMessage) {
    _initializeDefaultEncoderAndDecodersIfHasNotInitialized()
    const messageObject: IntermediateMessageType = {
        id: message.id,
        type: message.type,
        header: {},
    }
    if (message.type == MessageType.RpcResponse) {
        const _message = message as RpcResponse
        messageObject.requestId = _message.requestId
        messageObject.hasError = _message.hasError
        messageObject.error = _message.error
    }
    const bytesOfBody = isBufferObject(message.body) ? (message.header.set("content-type", "binary"),getBufferObject(message.body)) : (message.header.set("content-type", "json"),defaultTextEncoder.encode(JSON.stringify(message.body)))
    message.header.forEach((value, key) => {
        messageObject.header[key] = value
    })
    if (message.type == MessageType.RpcRequest) messageObject.action = (message as RpcRequest).action
    const bytes = Fragment.encodeAll(
        new Fragment(defaultTextEncoder.encode(JSON.stringify(messageObject))),
        new Fragment(bytesOfBody),
    )
    return bytes
}
/**
 * Decode given binary data to instance of {@type RpcMessage}
 * @param bytes binary data
 * @returns instance of {@type RpcMessage}
 */
function _decodeMessage(bytes: Uint8Array) {
    _initializeDefaultEncoderAndDecodersIfHasNotInitialized()
    const fragments: Fragment[] = [...Fragment.fromBytes(bytes) as any]
    const jsonOfMessage = defaultTextDecoder.decode(fragments[0].data)
    const messageObject: IntermediateMessageType = JSON.parse(jsonOfMessage)
    let message: RpcMessage = null!
    if (messageObject.type == MessageType.RpcRequest) {
        if (messageObject.action == null) throw new Error("'Action' is null on the rpc request message.")
        message = new RpcRequest(messageObject.id, messageObject.action!)
    }
    if (messageObject.type == MessageType.RpcResponse) {
        if (messageObject.requestId == null) throw new Error("'RequestId' is null on the rpc response message.")
        message = new RpcResponse(messageObject.id, messageObject.requestId!, messageObject.hasError ?? false, messageObject.error)
    }
    if (messageObject.type == MessageType.MetaRequest) {
        message = new MetaRequest(MetaData.typeSafeDataFromObject(JSON.parse(defaultTextDecoder.decode(fragments[1].data))))
    }
    if (messageObject.type == MessageType.MetaResponse) {
        message = new MetaResponse()
    }
    const isRpcCallMessage = messageObject.type == MessageType.RpcRequest || messageObject.type == MessageType.RpcResponse
    if (isRpcCallMessage) {
        const contentType = messageObject.header["content-type"]
        if (contentType == "json") {
            if (fragments[1].data.length > 0) {
                message.body = JSON.parse(defaultTextDecoder.decode(fragments[1].data))
            }
        }
        if (contentType == "binary") message.body = fragments[1].data
        if (["json", "binary"].filter(item => item == contentType).length == 0) throw new Error(`This content type '${contentType}' is not supported, Header 'content-type' is should be either 'json' or 'binary'`)
    }
    for (const key in messageObject.header) message.header.set(key, messageObject.header[key])
    return message
}
/**
 * Make an can be handle proxy object from meta data
 * @param proxyObject the all methods defined by metadata is will be add to this object as property, usually you this object is an empty object (the mean is '{}') just fine.
 * @param metaData meta data
 * @param handler handler that handle proxy object method's call
 * @param parentModuleName module path, usually if the supplied object is root object of this proxy object then this argument is can be null or "" empty string
 */
export function createProxyObjectWithMetaData(proxyObject: any, metaData: MetaData, handler: ({action, args}: {args: any, action: string}) => Promise<any>, parentModuleName: string = "") {
    if (metaData.type == MetaType.RpcMethod) {
        proxyObject[metaData.name] = async (args: any) => await handler({args, action: parentModuleName})
    }
    if (metaData.type == MetaType.RpcModule) {
        let module = proxyObject;
        if ((metaData.name ?? "") != "") {
            module = {}
            proxyObject[metaData.name] = module
        };
        (metaData as MetaOfModule).children.forEach(meta => {
            createProxyObjectWithMetaData(module, meta, handler, `${parentModuleName}/${meta.name}`)
        })
    }
}
/**
 * Get an function from given proxy object by it's path.
 * @param proxyObject proxy object
 * @param action action path, for example: "/module/subModule/functionName"
 * @returns async function or null
 */
export function getProxyObjectPropertyByActionName(proxyObject: any, action: string): any {
    const paths = action.split("/")
    let value = proxyObject
    let owner = value
    paths.forEach(path => {
        if (path == "") return
        owner = value
        value = value[path]
        if (value == null) throw Error(`Action of object '${action}' is undefined or null.`)
    })
    if (typeof(value) == "function") value = value.bind(owner)
    return value
}
/**
 * Host events call by BridgeHost implementations
 */
export class BridgeHostEvents {
    /**
     *
     * @param onReady Must be call when host is ready to use
     * @param onError Must be call when an error occur on host
     * @param onClose Must be call  when the host is closed
     * @param onUnableToConnect Must be call when unable to connect remote side (for example: remote socket).
     * @param onMessage Must be call
     */
    constructor(
        public onReady: () => Promise<void>,
        public onError: (error: Error) => Promise<void>,
        public onClose: () => Promise<void>,
        public onUnableToConnect: () => Promise<void>,
        public onMessage: (data: Uint8Array) => Promise<void>
    ) {
    }
}
/**
 * Standards for RpcBridgeHost
 * @remarks especially you will use if you want to define your own BridgeHost like "WebSocketBridgeHost", "TcpSocketBridgeHost", "HttpLongPoolingBridgeHost"
 */
export interface IRpcBridgeHost<BACKEND> {
    /**
     * backend object underlying this host
     * @remarks This is optional and NOT required anywhere
     * */
    backend: BACKEND
    /**this function is called by {@link RpcBridge} class when it is prepare to work over this host*/
    prepare(eventHandler: BridgeHostEvents): Promise<void>
    /**this function is called by {@link RpcBridge} class when it is attempt to open it's connection over this host */
    open(): Promise<void>
    /**this function is called by {@link RpcBridge} class when it is attempt to close it's connection over this bridge host */
    close(): Promise<void>
    /**this function is called by {@link RpcBridge} class when it is send data over this bridge host*/
    send(data: Uint8Array): Promise<void>
}
/**
 * Internal connection state of RpcBridge class
 */
enum BridgeConnectionState {
    CONNECTED = 0x01,
    CONNECTING = 0x02,
    DISCONNECTED = 0x03
}
/**
 * RpcBridge traffic hook
 */
export class RpcBridgeTrafficHook {
    /**
     * Call this method when a {@link RpcRequest} message
     * @param message message
     * @param action actual action in {@link RpcBridge} class
     * @remarks the action is MUST be call inside this method
     */
    public async callRequest(message: RpcRequest, action: (request: RpcRequest)=> Promise<void>): Promise<void> {
        /**Before processing */
        await action(message)
        /**After processing */
    }
    /**
     * Call this method when a RpcResponse message
     * @param message message
     * @param action actual action in RpcBridge class
     * @remarks the action is MUST be call inside this method
     */
    public async callResponse(message: RpcResponse, action: (response: RpcResponse) => Promise<void>): Promise<void> {
        await action(message)
    }
    /**
     * Call this method when a MetaRequest message
     * @param message message
     * @param action actual action in RpcBridge class
     * @remarks the action is MUST be call inside this method
     */
    metaDataRequest(message: MetaRequest, action: (request: MetaRequest) => Promise<void>): Promise<void> {
        return action(message)
    }
    /**
     * Call this method when a MetaResponse message
     * @param message message
     * @param action actual action in RpcBridge class
     * @remarks the action is MUST be call inside this method
     */
    metaDataResponse(message: MetaResponse, action: (response: MetaResponse) => Promise<void>): Promise<void> {
        return action(message)
    }
}
/**
 * RpcBridge hook for sending/receiving messages
 */
export class RpcBridgeHook {
    /**While Sending */
    sending = new RpcBridgeTrafficHook()
    /**While receiving */
    receiving = new RpcBridgeTrafficHook()
}
/**
 * Automatic catch unhandled promise errors and resolve double-initialize, double-close errors.
 */
class ErrorDetectorBridgeHost implements IRpcBridgeHost<IRpcBridgeHost<any>> {
    private _eventHandler: BridgeHostEvents = null!;
    public constructor(public backend: IRpcBridgeHost<any>) {}
    private _hasClosed = false
    private _hasInitialized = false
    public prepare(eventHandler: BridgeHostEvents): Promise<void> {
        this._eventHandler = eventHandler
        return this.backend.prepare(new BridgeHostEvents(
            async () => {
                if (this._hasInitialized) return
                this._hasInitialized = true
                await eventHandler.onReady()
            },
            eventHandler.onError,
            async () => {
                if (this._hasClosed) return
                this._hasClosed = true
                await eventHandler.onClose()
            },
            eventHandler.onUnableToConnect,
            eventHandler.onMessage
        ))
    }
    public async open(): Promise<void> {
        try {
            await this.backend.open()
        }
        catch (e) {
            await this._eventHandler.onError(e)
            await this.close();
        }
    }
    public async close(): Promise<void> {
        if (this._hasClosed) return;
        this._hasClosed = true
        return await this.backend.close()
    }
    public async send(data: Uint8Array): Promise<void> {
        try {
            await this.backend.send(data)
        } catch (error) {
            //Close
            await this._eventHandler.onError(error)
            await this.close()
        }
    }

}
/**
 * Bridge that represent a bridge between local and remote connection
 * @remarks
 * This is the one of core object in this framework
 * using this class, you can invoke remote functions on remote side (maybe server or client)
 */
export class RpcBridge<LOCAL_INTERFACE, REMOTE_INTERFACE> {
    /**
     * Proxy object that represent remote side interface implementation
     * @remarks through this object, you can access remote side methods directly.
     */
    public remoteInterface: REMOTE_INTERFACE = null!
    /**
     * Trig when an error occur on underlying connection, maybe this time is correspond connection error or unable to connect
     * @param error error object
     */
    public onCommunicationError = async (error: Error) => {console.error(error)}
    /**
     * Trig when the underlying connection is closed after connection success.
     * @remarks this is helpful when you want to implement reconnection functionality after connection is lost.
     */
    public onCommunicationClosed = async () => {}
    /**
     * Trig when underlying connection is reestablished
     */
    /**
     * Fired when unable to connect to remote connection.
     */
    public onUnableToConnect = async () => {}
    public onCommunicationRecovered = async () => {}
    private _localInterfaceImplementationMetaData: MetaData
    private _waitForResponseQueue = new Map<string, Completer<any>>()
    private _bridgeState = BridgeConnectionState.DISCONNECTED
    private _isInitializedOnce = false
    private host: IRpcBridgeHost<any> = null!
    /**
     * Hook object for traffics
     */
    public hook = new RpcBridgeHook()
    private _remoteInterfaceDelegateObjectReadyCompleter: Completer<void> = Completer.create()
    private _hasMetaDataReceivedByOtherSideCompleter: Completer<void> = Completer.create()
    /**
     * Create instance
     * @param host instance of {@link IRpcBridgeHost} object underlying this bridge
     * @param localInterface an instance of local interface implementation, this is will be used by remote side.
     */
    public constructor(host: IRpcBridgeHost<any>, public localInterface: LOCAL_INTERFACE) {
        this.host = new ErrorDetectorBridgeHost(host)
        this._localInterfaceImplementationMetaData = MetaData.detectFromObject(localInterface)
    }
    /**
     * Initializing promise, users can wait this promise for waiting for initialize.
     */
    public get initializingPromise(): Promise<void> {
        return new Promise<void>(async (resolve, reject) => {
            try {
                await this._remoteInterfaceDelegateObjectReadyCompleter.promise
                await this._hasMetaDataReceivedByOtherSideCompleter.promise
                resolve()
            }
            catch (error) {
                reject(error)
            }
        })
    }
    /**
     * Should be call before perform initialize operation, this is only used by internal, users don't call this itself.
     * @private
     */
    private async _beforePerformInitialize() {
        this._bridgeState = BridgeConnectionState.CONNECTING
        this._hasMetaDataReceivedByOtherSideCompleter = Completer.create()
        this._remoteInterfaceDelegateObjectReadyCompleter = Completer.create()
    }
    /**
     * Should be call after perform initialize operation, when call this function is finished then that moment corresponds initialize ready, this is only used by internal, users don't call this itself.
     * @private
     */
    private async _afterPerformInitialize() {
        await this.initializingPromise
        this._bridgeState = BridgeConnectionState.CONNECTED
    }
    /**
     * Connect to remote side
     */
    public async connect(): Promise<void> {
        if (this._bridgeState != BridgeConnectionState.DISCONNECTED) throw new Error("Bridge is already connected (or being to connecting)")
        await this._beforePerformInitialize()
        const handler = new BridgeHostEvents(
            async () => {
                if (this._isInitializedOnce) { //if connection is already connected and reconnect after disconnecting.
                    await this._beforePerformInitialize()
                    this._afterPerformInitialize().then(async _ => {
                        await this.onCommunicationRecovered()
                    })
                }
                const sendMetaData = async () => {
                    const message = new MetaRequest(this._localInterfaceImplementationMetaData)
                    await this.hook.sending.metaDataRequest(message, async message => {
                        const packet = message.encode()
                        await this.host.send(packet)
                    })
                }
                this._isInitializedOnce = true
                await sendMetaData()
            },
            async error => {
                this._waitForResponseQueue.forEach((value, key) => {
                    value.error(error)
                })
                await this.onCommunicationError(error)
            },
            async () => {
                this._waitForResponseQueue.forEach(item => item.error(new Error("Connection closed before call responds.")))
                this._waitForResponseQueue.clear()
                this._bridgeState = BridgeConnectionState.DISCONNECTED
                await this.onCommunicationClosed()
            },
            async () => {
                await this.onUnableToConnect()
            },
            async data => {
                const message = RpcMessage.decode(data)
                if (message.type == MessageType.MetaRequest) {
                    await this.hook.receiving.metaDataRequest(message as MetaRequest, async message => {
                        await this._onMetaDataReceived(message.meta)
                        this._remoteInterfaceDelegateObjectReadyCompleter.complete()
                    })
                    this._afterPerformInitialize()
                }
                if (message.type == MessageType.MetaResponse) {
                    await this.hook.receiving.metaDataResponse(message, async message => {
                        this._hasMetaDataReceivedByOtherSideCompleter.complete()
                    })
                    this._afterPerformInitialize()
                }
                if (message.type == MessageType.RpcRequest) {
                    await this.hook.receiving.callRequest(message as RpcRequest, async message => {
                        await this._onRpcRequestReceived(message)
                    })
                }
                if (message.type == MessageType.RpcResponse) {
                    await this.hook.receiving.callResponse(message as RpcResponse, async message => {
                        await this._onRpcResponseReceived(message)
                    })
                }
            }
        )
        await this.host.prepare(handler)
        this.host.open()
        await this._afterPerformInitialize()
    }
    /**
     * Disconnect this (and underlying) connection
     */
    public async disconnect() {
        if (this._bridgeState == BridgeConnectionState.CONNECTING) throw new Error("Please try disconnect connection after connection is successfully.")
        if (this._bridgeState == BridgeConnectionState.DISCONNECTED) throw new Error("connection is already disconnected, you have no need to disconnect again.")
        await this.host.close()
        this._bridgeState = BridgeConnectionState.DISCONNECTED
    }
    /**
     * when meta data received from other side.
     * @param meta meta data
     */
    private async _onMetaDataReceived(meta: MetaData) {
        this.remoteInterface = {} as any
        createProxyObjectWithMetaData(this.remoteInterface, meta, async (event: {args: any, action: string}) => {
            const message = new RpcRequest(uuid(), event.action)
            const completer = Completer.create<any>()
            message.body = event.args
            await this.hook.sending.callRequest(message, async message => {
                const packet = message.encode()
                this._waitForResponseQueue.set(message.id, completer)
                await this.host.send(packet)
            })
            return await completer.promise //should be wait until remote rpc call finished and RpcCallResponse received.

        })
        const message = new MetaResponse()
        await this.hook.sending.metaDataResponse(message, async message => {
            await this.host.send(message.encode())
        })
    }
    /**
     * When received rpc call request by remote side.
     * @param request Rpc call request
     */
    private async _onRpcRequestReceived(request: RpcRequest) {
        const message = new RpcResponse(uuid(), request.id, false)
        try {
            const action = getProxyObjectPropertyByActionName(this.localInterface, request.action)
            message.body = await action(request.body)
        } catch (e) {
            message.hasError = true
            message.error = e.message ?? e
        }
        this.hook.sending.callResponse(message, async message => {
            await this.host.send(message.encode())
        })
    }
    /**
     * When rpc call response received from remote side.
     * @param response Rpc response
     */
    private async _onRpcResponseReceived(response: RpcResponse) {
        const completer = this._waitForResponseQueue.get(response.requestId)
        if (completer == null) throw new Error(`This request completer '${response.requestId}' from queue is not exists, maybe this program (or remote side) has bug.`)
        if (response.hasError) {
            completer.error(new Error(`An error occurred on remote side while call rpc method, here is the error message: "${response.error}"`))
        } else {
            completer.complete(response.body)
        }
        this._waitForResponseQueue.delete(response.requestId)
    }
}
